上一篇先介紹運用的知識點,這篇會著重在實作時的心路歷程...不是啦,是怎麼把這個網頁寫出來的。先上成品與程式碼,若程式有寫得太過繁瑣的部分,也希望大家多包涵並不吝指教:
github // github page
要寫文章時回頭看整個程式碼,覺得好像也沒用到什麼技術,但剛開始一片空白要寫出東西還真的毫無頭緒。有 bug 卡住的時候,明明覺得答案近在咫尺但就是想不到也很崩潰(不愧是 JS 小菜雞)。因此才想要補充這篇文章,如果你也正因為卡住在找答案的話,希望能幫到忙(不是倒忙)。
在開始前要先釐清需求,我想要做出這些功能:
看起來很多,所以先求有再求好,至少先讓內容能在輸入後被加到下方吧!
當使用者填好資料後,按下加號,要讓 JS 操控 HTML 新增一條 todo 到畫面上,架構應該如下一樣的呈現:
<ul class="todo-list">
<li class="todo"> <!--變數名稱 todoLi-->
<ul class="todo-item"> <!--變數名稱 newTodo-->
<li class="todo-date">Date</li> dateInput.value 輸入什麼就顯示什麼 <!--變數名稱 newTodoDate-->
<li class="todo-time">Time</li> timeInput.value 輸入什麼就顯示什麼 <!--變數名稱 newTodoTime-->
<li class="todo-sort">Sort</li> sortSelect.value 輸入什麼就顯示什麼 <!--變數名稱 newTodoSort-->
<li class="todo-detail">item detail</li> todoInput.value 輸入什麼就顯示什麼 <!--變數名稱 newTodoDetail-->
</ul>
<div class="todo-btn"> <!--變數名稱 newTodoButton-->
<button class="complete-btn"></button> <!--變數名稱 completedButton-->
<button class="complete-btn"></button> <!--變數名稱 trashButton-->
</div>
</li>
</ul>
之所以要先想好架構,是因為要新增的東西很多,如果邊做邊想很容易搞亂。由此會想到昨天介紹的 createElement() 、 appendChild()。因此便可以照著剛剛想好的架構開始組裝並加上 class 。
除了上述之外,還有幾個小點要注意:
if (todoInput.value == 0 || todoInput.value == undefined || todoInput.value == null){
alert("內容欄為必填");
}else if(dateInput.value ==0 || dateInput.value == undefined || dateInput.value == null){
alert("日期欄為必填");
}else if(timeInput.value == 0 || timeInput.value == undefined || timeInput.value == null){
alert("時間欄為必填");
}else{
//選什麼種類就秀對應圖案
if(taskSort.value == "job"){
newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
}else if(taskSort.value == "housework"){
newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
}else if(taskSort.value == "sport"){
newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
}else if(taskSort.value == "routine"){
newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
}else if(taskSort.value == "others"){
newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
}else{
newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
};
//add todo to localstorage
let saveLocal = [dateInput.value,timeInput.value,taskSort.value,todoInput.value];
saveLocalTodos(saveLocal);
之後你就會得到:
//selectors
const dateInput = document.querySelector('#date-input');
const timeInput = document.querySelector('#time-input');
const todoInput = document.querySelector('.todo-input');
const addButton = document.querySelector('.addlist-button');
const todoList = document.querySelector('#todoList');
const taskSort = document.querySelector('#task-sort');
//event listeners
addButton.addEventListener('click',addTodo);
todoList.addEventListener('click',deleteCheck);
//functions
function addTodo(event){
event.preventDefault();
const todoLi = document.createElement('li');
todoLi.classList.add("todo");
const newTodo = document.createElement('ul');
newTodo.classList.add("todo-item");
todoLi.appendChild(newTodo);
const newTodoDate = document.createElement('li');
const newTodoTime = document.createElement('li');
const newTodoSort = document.createElement('li');
const newTodoDetail = document.createElement('li');
newTodoDate.classList.add("todo-date");
newTodoTime.classList.add("todo-time");
newTodoSort.classList.add("todo-sort");
newTodoDetail.classList.add("todo-detail");
if (todoInput.value == 0 || todoInput.value == undefined || todoInput.value == null){
alert("內容欄為必填");
}else if(dateInput.value ==0 || dateInput.value == undefined || dateInput.value == null){
alert("日期欄為必填");
}else if(timeInput.value == 0 || timeInput.value == undefined || timeInput.value == null){
alert("時間欄為必填");
}else{
newTodoDate.innerText = dateInput.value;
newTodoTime.innerText = timeInput.value;
newTodoDetail.innerText = todoInput.value;
if(taskSort.value == "job"){
newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
}else if(taskSort.value == "housework"){
newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
}else if(taskSort.value == "sport"){
newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
}else if(taskSort.value == "routine"){
newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
}else if(taskSort.value == "others"){
newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
}else{
newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
};
newTodo.appendChild(newTodoDate);
newTodo.appendChild(newTodoTime);
newTodo.appendChild(newTodoSort);
newTodo.appendChild(newTodoDetail);
let saveLocal = [dateInput.value,timeInput.value,taskSort.value,todoInput.value];
saveLocalTodos(saveLocal);
const newTodoButton = document.createElement('div');
newTodoButton.classList.add("todo-btn");
todoLi.appendChild(newTodoButton);
const completedButton = document.createElement('button');
completedButton.innerHTML = '<i class="fas fa-check"></i>';
completedButton.classList.add('complete-btn');
newTodoButton.appendChild(completedButton);
const trashButton = document.createElement('button');
trashButton.innerHTML = '<i class="fas fa-trash"></i>';
trashButton.classList.add('danger-btn');
newTodoButton.appendChild(trashButton);
todoList.appendChild(todoLi);
location.reload();
dateInput.value = "";
timeInput.value = "";
todoInput.value = "";
};
}
接著是完成和刪除鍵的功能撰寫。
在 To Do List 練習中,將會一直牽涉到 HTML DOM 中的查找,可以善用 console.log 來確定我們要取得的是不是跟我們打的一樣。
按刪除鍵時,想達成的目的是要刪除一整條的 todoLi 。用console.log(e.target);
可以發現, e.target 等於我們點的位置的 html 標籤,意即若直接 remove 掉 e.target,移除的會是刪除鈕本身,因此須回到父層再刪除,同理按完成鍵也是一樣概念,因此可將它們放在同一函式中。但要怎麼分別刪除和完成呢?
沒錯,同樣是要運用 DOM 觀念查找,發現可以從 item.classList[0] ,也就是 item 的第一層 class 為 danger-btn 或 complete-btn 來區分,進而執行不同的事。
刪除的部分,我們要幫他加動畫效果 fall ,並用 css 設定 fall 這個動畫的細節。在動畫跑完後,才執行函式將 todo 本身移除。並且要讓本地端儲存的資料一併刪除。但是本地端的部分,讓我們稍後再細談。
完成鍵的部分,要在 todo 這個 div 加上 completed 的 class,藉此設定畫線樣式。讓本地端儲存的資料一併刪除,但紀錄完成了哪些事。
function deleteCheck(e){
const item = e.target;
const todo = item.parentElement.parentElement;
//delete btn
if(item.classList[0] === 'danger-btn'){
todo.classList.add("fall");
removeLocalTodos(todo);
todo.addEventListener('transitionend',function(){
todo.remove();
});
}
//check btn
if(item.classList[0] === 'complete-btn'){
todo.classList.add('completed');
let date = todo.querySelector(".todo-date").innerText;
let time = todo.querySelector(".todo-time").innerText;
let detail = todo.querySelector(".todo-detail").innerText;
let sort = todo.querySelector(".todo-sort").innerHTML;
if (sort == `<i class="fas fa-briefcase"></i>`){
sort = "job";
}else if(sort == `<i class="fas fa-home"></i>`){
sort = "housework";
}else if(sort == `<i class="far fa-futbol"></i>`){
sort = "sport";
}else if(sort == `<i class="fas fa-hourglass"></i>`){
sort = "routine";
}else{
sort = "others";
};
let saveLocalComplete = [date,time,sort,detail];
saveLocalCompleteTodos(saveLocalComplete);
removeLocalTodos(todo);
}
}
先停一下,來處理儲存與刪除本地端資料的問題。要儲存的值會有三項:還沒完成的項目、已完成的項目、完成的數目。可以分別將三個 key 命名為 todo 、 complete 和 completeTask ,並分別儲存。網頁重整時,再從本地端提出來。
為了讓 completeTask 的數量等同目前存在於 complete 陣列中的數, completeTodos.length 派上用場。最後,按完完成鍵時,設定它在一定時間後自動重整頁面。
function saveLocalTodos(todo) {
let todos;
if (localStorage.getItem("todos") === null) {
todos = [];
} else {
todos = JSON.parse(localStorage.getItem("todos"));
}
todos.push(todo);
localStorage.setItem("todos", JSON.stringify(todos));
}
function saveLocalCompleteTodos(todo){
let completeTodos;
if (localStorage.getItem("complete") === null) {
completeTodos = [];
} else {
completeTodos = JSON.parse(localStorage.getItem("complete"));
}
completeTodos.push(todo);
localStorage.setItem("complete", JSON.stringify(completeTodos));
if (completeTodos.length != 0){
completedNum.innerHTML = `已完成 ${completeTodos.length} 項工作!`;
}else{
completedNum.innerHTML = `尚未有完成的工作!`;
}
localStorage.setItem("completeTask",JSON.stringify(completeTodos.length));
window.setTimeout(function () {
window.location.reload();
}, 500);
}
const completedNum = document.querySelector('#completedNum');
const clearCompleteNum = document.querySelector('#clearCompleteNum');
document.addEventListener('DOMContentLoaded',getTodos);
分別確認本地端有沒有 todos 和 completes ,沒有的就要建立空陣列。排列組合下會寫出四種出來。這時可用 console.log(todos); 檢查,發現 todo 已被分成一條 task 一個陣列的狀態,這時再複製上面 function addTodo ,只是記得將 input 改成 todo[n] / complete[n]。
也在這同步處理完成數量的程式:
if(completes = []){
completedNum.innerHTML = `尚未有完成的工作!`;
}else{
completedTotalNum = JSON.parse(localStorage.getItem('completeTask'));
completedNum.innerHTML = `已完成 ${completedTotalNum} 項工作!`;
}
時間排序及過期的設定先不管的話,現在的你應該會得到以下程式:
function getTodos(){
let todos;
let completes;
if(localStorage.getItem('todos') === null && localStorage.getItem('complete') === null){
todos = [];
completes = [];
}else if(localStorage.getItem('todos') === null && localStorage.getItem('complete') !== null){
todos = [];
completes = JSON.parse(localStorage.getItem("complete"));
}else if(localStorage.getItem('complete') === null && localStorage.getItem('todos') !== null){
todos = JSON.parse(localStorage.getItem('todos'));
completes = [];
}else if(localStorage.getItem('complete') !== null && localStorage.getItem('todos') !== null){
todos = JSON.parse(localStorage.getItem('todos'));
completes = JSON.parse(localStorage.getItem("complete"));
}
todos.forEach(function(todo) {
const todoLi = document.createElement('li');
todoLi.classList.add("todo");
const newTodo = document.createElement('ul');
newTodo.classList.add("todo-item");
todoLi.appendChild(newTodo);
const newTodoDate = document.createElement('li');
const newTodoTime = document.createElement('li');
const newTodoSort = document.createElement('li');
const newTodoDetail = document.createElement('li');
newTodoDate.classList.add("todo-date");
newTodoTime.classList.add("todo-time");
newTodoSort.classList.add("todo-sort");
newTodoDetail.classList.add("todo-detail");
newTodoDate.innerText = todo[0];
newTodoTime.innerText = todo[1];
newTodoDetail.innerText = todo[3];
if(todo[2] == "job"){
newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
}else if(todo[2] == "housework"){
newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
}else if(todo[2] == "sport"){
newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
}else if(todo[2] == "routine"){
newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
}else if(todo[2] == "others"){
newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
}else{
newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
};
newTodo.appendChild(newTodoDate);
newTodo.appendChild(newTodoTime);
newTodo.appendChild(newTodoSort);
newTodo.appendChild(newTodoDetail);
const newTodoButton = document.createElement('div');
newTodoButton.classList.add("todo-btn");
todoLi.appendChild(newTodoButton);
const completedButton = document.createElement('button');
completedButton.innerHTML = '<i class="fas fa-check"></i>';
completedButton.classList.add('complete-btn');
newTodoButton.appendChild(completedButton);
const trashButton = document.createElement('button');
trashButton.innerHTML = '<i class="fas fa-trash"></i>';
trashButton.classList.add('danger-btn');
newTodoButton.appendChild(trashButton);
todoList.appendChild(todoLi);
});
completes.forEach(function(complete){
const todoLi = document.createElement('li');
todoLi.classList.add("todo");
todoLi.classList.add("completed");
const completeTodo = document.createElement('ul');
completeTodo.classList.add("todo-item");
todoLi.appendChild(completeTodo);
const doneTodoDate = document.createElement('li');
const doneTodoTime = document.createElement('li');
const doneTodoSort = document.createElement('li');
const doneTodoDetail = document.createElement('li');
doneTodoDate.classList.add("todo-date");
doneTodoTime.classList.add("todo-time");
doneTodoSort.classList.add("todo-sort");
doneTodoDetail.classList.add("todo-detail");
completeTodo.appendChild(doneTodoDate);
completeTodo.appendChild(doneTodoTime);
completeTodo.appendChild(doneTodoSort);
completeTodo.appendChild(doneTodoDetail);
doneTodoDate.innerText = complete[0];
doneTodoTime.innerText = complete[1];
doneTodoDetail.innerText = complete[3];
if(complete[2] == "job"){
doneTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
}else if(complete[2] == "housework"){
doneTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
}else if(complete[2] == "sport"){
doneTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
}else if(complete[2] == "routine"){
doneTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
}else if(complete[2] == "others"){
doneTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
}else{
doneTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
};
todoList.appendChild(todoLi);
})
completedTotalNum = JSON.parse(localStorage.getItem('completeTask'));
if(completedTotalNum == null){
completedNum.innerHTML = `尚未有完成的工作!`;
}else{
completedNum.innerHTML = `已完成 ${completedTotalNum} 項工作!`;
}
};
順帶一提,因為上面先寫 todos.forEach 再寫 completes.forEach ,還沒完成的會放在上面,已經完成的會被擺到下面。若不太懂我的意思,你可以把兩個順序倒過來試試,就會知道我在說什麼。
因為 todos 是陣列包著陣列, 而 todo 是點下去的那段的程式碼,所以把 todo 轉成跟 todos 一樣的陣列方式(有點類似剛剛儲存時轉過來,現在再轉回去)。
接著要用 indexOf 找到他在陣列上是第幾個位置,然後用 slice 把它切掉。剛剛提過了, todos 是陣列中又包著陣列。當要在陣列中包著陣列的形式中尋找特定陣列,使用 indexOf 會找不到,因為 indexOf 是用嚴格模式判斷,例如即使 todos=[[3,0],[1,2]] ,找 todos.indexOf([3,0]) 也找不到。為此需要客製化 indexOf :
既然陣列是從 0 開始數,預設代表陣列位置的變數 i=0 。當 i 小於查找項目的長度,跑下面的迴圈,跑完加一再繼續跑,直到等於長度時停止。從查找陣列的第 0 項開始,當第 0 項陣列中的第 0 個位置的值,跟要找的陣列的第 0 個值相同,就讓 i 顯示 0 返回,藉此得知要找的就在第 0 的位置,依此類推,若都找不到則返回 -1。
終於寫好。依上面寫好的程式代入 todo (被找的父陣列) 和 todoIndex (要找的內容)。1 的位置要填的數字,代表要刪幾個,只刪一個所以填一。最後,把結果傳回本地端。
function removeLocalTodos(todo){
let todos;
if(localStorage.getItem('todos') === null){
todos = [];
}else{
todos = JSON.parse(localStorage.getItem('todos'));
}
let date = todo.querySelector(".todo-date").innerText;
let time = todo.querySelector(".todo-time").innerText;
let detail = todo.querySelector(".todo-detail").innerText;
let sort = todo.querySelector(".todo-sort").innerHTML;
if (sort == `<i class="fas fa-briefcase"></i>`){
sort = "job";
}else if(sort == `<i class="fas fa-home"></i>`){
sort = "housework";
}else if(sort == `<i class="far fa-futbol"></i>`){
sort = "sport";
}else if(sort == `<i class="fas fa-hourglass"></i>`){
sort = "routine";
}else{
sort = "others";
};
let todoIndex = [date,time,sort,detail];
function indexOfCustom (parentArray, searchElement) {
for (let i = 0; i < parentArray.length; i++ ) {
if ( parentArray[i][0] == searchElement[0] && parentArray[i][1] == searchElement[1] && parentArray[i][2] == searchElement[2] && parentArray[i][3] == searchElement[3]) {
return i;
}
}
return -1;
}
todos.splice(indexOfCustom(todos,todoIndex),1);
localStorage.setItem("todos",JSON.stringify(todos));
}
雖然文章和程式很長,但其實可以發現都是一些簡單的觀念重複運用,只有少數幾個小卡關的點而已。而我們沒達成的需求還剩: